﻿using System;
using System.Globalization;
using System.Data;
using System.Text;
using System.Net;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using SharePointSearchBench.Library.QueryService;
using Microsoft.Office.Server.Search.Query;
using Microsoft.SharePoint;
using Microsoft.Office.Server.Search.Administration;
using Microsoft.Office.Server;
using SharePointSearchBench.Library.Logging;

namespace SharePointSearchBench.Library
{
    /// <summary>
    /// Executes searches against Microsoft Office SharePoint Server
    /// using either object model or web service calls.
    /// </summary>
    /// <remarks>This is the primary class used to execute searches with
    /// SharePoint Search Bench.</remarks>
    /// <example>
    /// <code>
    /// // example object model keyword query
    /// Search search = new Search();
    /// search.SearchType = SearchType.Keyword;
    /// search.SearchText = new SearchText("dog cat llama");
    /// Uri siteUri = new Uri("http://SharePointSite");
    /// search.ContextUri = siteUri;
    /// search.ApiSource = ApiSource.ObjectModel;
    /// DataSet results = search.Execute();
    /// </code>
    /// </example>
    /// <example>
    /// <code>
    /// // example service full text query
    /// Search search = new Search();
    /// search.SearchType = SearchType.FullText;
    /// NetworkCredential credentials = CredentialCache.DefaultCredentials;
    /// Uri serviceUri = new Uri("http://SharePointSite/_vti_bin/Search.asmx");
    /// string text = "SELECT Title, Author FROM Scope() " +
    ///     "WHERE FREETEXT(DEFAULTPROPERTIES, 'dog cat llama') " +
    ///     "ORDER BY RANK";
    /// search.SearchText = new SearchText(text);
    /// search.Credentials = credentials;
    /// search.ApiSource = ApiSource.Service;
    /// search.ContextUri = serviceUri;
    /// DataSet results = search.Execute();    </code>
    /// </example>
    public class Search : BaseQuery
    {
        SearchType searchType;
        SearchText searchText;
        SearchResultType searchResultType;
        int startAt;
        StringCollection displayFields;
        StringCollection sortFields;
        string finalQueryXml;
        Collection<SearchProperty> searchProperties;
        Collection<SearchScope> searchScopes;
        ISearchBenchContext searchBenchContext;
        bool userChangedSearchContext;

        /// <summary>
        /// Creates a new instance of the class.
        /// </summary>
        public Search()
        {
            this.startAt = 1;
            this.searchResultType = SearchResultType.Relevant;
            this.displayFields = new StringCollection();
            this.sortFields = new StringCollection();
        }

        /// <summary>
        /// Gets the query packet generated from the last web service call.
        /// </summary>
        public string FinalQueryXml
        {
            get { return this.finalQueryXml; }
            set { this.finalQueryXml = value; }
        }

        /// <summary>
        /// Gets the collection of fields that should be returned
        /// in search results.
        /// </summary>
        public StringCollection DisplayFields
        {
            get { return this.displayFields; }
        }

        /// <summary>
        /// Gets the collection of fields that dictate the
        /// sort order in search results.  
        /// </summary>
        /// <remarks>Only an ascending sort order is supported at this time.</remarks>
        public StringCollection SortFields
        {
            get { return this.sortFields; }
        }

        /// <summary>
        /// Gets or sets the index of the result to begin the result set at.
        /// </summary>
        public int StartAt
        {
            get { return this.startAt; }
            set { this.startAt = value; }
        }



        /// <summary>
        /// Gets or sets the type of results to return.
        /// </summary>
        public SearchResultType SearchResultType
        {
            get { return this.searchResultType; }
            set { this.searchResultType = value; }
        }

        /// <summary>
        /// Gets or sets whether keyword or full text search is used.
        /// </summary>
        public SearchType SearchType
        {
            get { return this.searchType; }
            set { this.searchType = value; }
        }


        /// <summary>
        /// Gets or sets the keyword phrase or the full text
        /// query for the search.
        /// </summary>
        public SearchText SearchText
        {
            get { return this.searchText; }
            set { this.searchText = value; }
        }

        /// <summary>
        /// Gets the <see cref="SearchProperty"/> objects that describe what
        /// properties are available for using in full text searches.
        /// </summary>
        public Collection<SearchProperty> SearchProperties
        {
            get
            {
                if (this.searchProperties == null)
                {
                    this.searchProperties = this.GetProperties();
                }
                return this.searchProperties;
            }
        }

        /// <summary>
        /// Gets the <see cref="SearchScope"/> objects that describe what
        /// scopes are available for searching.
        /// </summary>
        public Collection<SearchScope> SearchScopes
        {
            get
            {
                if (this.searchScopes == null)
                {
                    this.searchScopes = this.GetScopes();
                }
                return this.searchScopes;
            }
        }

        /// <summary>
        /// Resets the search context so that the default search contexts are used
        /// for object model and service searches.  If a user or developer has specified
        /// a custom search context, it is removed and replaced.
        /// </summary>
        public void SetSearchContextToDefault()
        {
            this.userChangedSearchContext = false;
            this.SetPreferredSearchBenchContext();
        }

        void SetPreferredSearchBenchContext()
        {
            using (IDisposableLogWriter debug = new DisposableDebugLogWriter())
            {
                if (!this.userChangedSearchContext)
                {
                    if (this.ApiSource == ApiSource.ObjectModel)
                    {
                        this.searchBenchContext = new ServerSearchBenchContext(this);
                    }
                    else
                    {
                        this.searchBenchContext = new ServiceSearchBenchContext(this);
                    }
                    debug.WriteMessage("Default search bench context set to type '" + this.searchBenchContext.GetType().FullName + "'");
                }
            }
        }

        /// <summary>
        /// Sets the running context for searches.  This method should be used to
        /// "swap out" the default context during testing.
        /// </summary>
        /// <param name="context"></param>
        public void SetSearchBenchContext(ISearchBenchContext context)
        {
            this.searchBenchContext = context;
            this.userChangedSearchContext = true;
            DebugLogWriter.WriteMessage("Search bench context set to type '" + this.searchBenchContext.GetType().FullName + "'");
        }

        /// <summary>
        /// Performs a search against Microsoft Office SharePoint Server.
        /// </summary>
        /// <returns>A <see cref="System.Data.DataSet"/> containing a single table of results.  The
        /// table columns map to the <see cref="DisplayFields"/> that were specified.</returns>
        public DataSet Execute()
        {
            using (IDisposableLogWriter debug = new DisposableDebugLogWriter("Execute"))
            {
                if (this.SearchText == null)
                {
                    ErrorLogWriter.WriteMessage("SearchText was not set.");
                    throw new MissingSearchTextException();
                }

                if (this.ContextUri == null)
                {
                    ErrorLogWriter.WriteMessage("ContextUri was not set.");
                    throw new MissingContextUriException();
                }

                if (this.ApiSource == ApiSource.Service
                    && this.Credentials == null)
                {
                    ErrorLogWriter.WriteMessage("Credentials were not set for a service call.");
                    throw new MissingCredentialsException();
                }

                DataSet results = null;


                string format = "{0}: {1}";
                debug.WriteMessage("Search properties...");
                debug.WriteMessage(string.Format(format, "ContextUri", this.ContextUri.ToString()));
                debug.WriteMessage(string.Format(format, "ApiSource", this.ApiSource.ToString()));
                debug.WriteMessage(string.Format(format, "SearchType", this.SearchType.ToString()));
                if (!string.IsNullOrEmpty(this.SearchText.ToString()))
                {
                    debug.WriteMessage(string.Format(format, "SearchText", this.SearchText.ToString()));
                }
                else
                {
                    debug.WriteMessage("SearchText was null or empty.");
                }

                //set context
                if (!this.userChangedSearchContext)
                {
                    this.SetPreferredSearchBenchContext();
                }

                debug.WriteMessage("Preparing search...");
                this.searchBenchContext.PrepareSearch();
                debug.WriteMessage("Executing search...");
                results = this.searchBenchContext.Execute();
                debug.WriteMessage("Search completed.  Found " + results.Tables[0].Rows.Count.ToString() + " results.");
                return results;
            }
        }


        Collection<SearchProperty> GetProperties()
        {
            this.SetPreferredSearchBenchContext();
            if (this.ContextUri == null)
            {
                throw new MissingContextUriException();
            }

            if (this.searchBenchContext.RequiresCredentials && this.Credentials == null)
            {
                throw new MissingCredentialsException();
            }

            return this.searchBenchContext.GetSearchProperties();
        }

        Collection<SearchScope> GetScopes()
        {
            using (IDisposableLogWriter debug = new DisposableDebugLogWriter())
            {
                debug.WriteMessage("Setting preferred search bench context");
                this.SetPreferredSearchBenchContext();
                if (this.searchBenchContext.RequiresCredentials && this.Credentials == null)
                {
                    throw new MissingCredentialsException();
                }

                if (this.ContextUri == null)
                {
                    throw new MissingContextUriException();
                }

                debug.WriteMessage("Getting scopes from context");
                return this.searchBenchContext.GetScopes();
            }
        }

        /// <summary>
        /// Converts a <see cref="SearchResultType"/> into a Microsoft ResultType enumeration.
        /// </summary>
        /// <param name="searchResultType"></param>
        /// <returns></returns>
        public static ResultType GetSharePointResultType(SearchResultType searchResultType)
        {
            switch (searchResultType)
            {
                case SearchResultType.Relevant:
                    return ResultType.RelevantResults;
                case SearchResultType.DefinitionResults:
                    return ResultType.DefinitionResults;
                case SearchResultType.SpecialTerm:
                    return ResultType.SpecialTermResults;
                case SearchResultType.HighConfidence:
                    return ResultType.HighConfidenceResults;
                default:
                    return Microsoft.Office.Server.Search.Query.ResultType.RelevantResults;
            }
        }

        internal DataSet CreateDataSetFromResults(ResultTableCollection results)
        {
            DataSet resultSet = new DataSet();
            resultSet.Locale = CultureInfo.CurrentCulture;
            DataTable table = resultSet.Tables.Add();
            table.TableName = "Results";

            ResultType sharePointResultType = Search.GetSharePointResultType(this.SearchResultType);
            ResultTable resultTable = results[sharePointResultType];

            //add columns
            for (int i = 0; i < resultTable.FieldCount; i++)
            {
                string name = resultTable.GetName(i);
                Type columnType = resultTable.GetFieldType(i);
                DataColumn column = new DataColumn(name, columnType);
                table.Columns.Add(column);
            }

            //add rows
            while (resultTable.Read())
            {
                DataRow row = table.NewRow();
                for (int i = 0; i < resultTable.FieldCount; i++)
                {
                    string name = resultTable.GetName(i);
                    if (resultTable.GetValue(i) == null)
                    {
                        row[name] = DBNull.Value;
                    }
                    else
                    {
                        row[name] = resultTable.GetValue(i);
                    }
                }
                table.Rows.Add(row);
            }

            return resultSet;
        }


        /// <summary>
        /// Builds an Xml document to pass into a SharePoint Search
        /// service Query method for keyword searches.
        /// </summary>
        /// <returns></returns>
        public string BuildKeywordQueryPacket()
        {
            StringBuilder packet = new StringBuilder();
            packet.Append("<?xml version=\"1.0\" encoding=\"utf-8\" ?>");
            packet.Append("<QueryPacket xmlns=\"urn:Microsoft.Search.Query\" Revision=\"1000\">");
            packet.Append("<Query domain=\"QDomain\">");
            packet.Append("<SupportedFormats>");
            packet.Append("<Format>urn:Microsoft.Search.Response.Document.Document</Format>");
            packet.Append("</SupportedFormats>");
            packet.Append("<Context>");
            packet.Append("<QueryText language=\"en-US\" type=\"STRING\">");
            packet.Append(this.SearchText);
            packet.Append("</QueryText>");
            packet.Append("</Context>");
            packet.Append("<Range>");
            packet.Append("<StartAt>");

            NumberFormatInfo format = CultureInfo.CurrentCulture.NumberFormat;

            packet.Append(this.StartAt.ToString(format));
            packet.Append("</StartAt>");
            packet.Append("<Count>");
            packet.Append(this.ResultCount.ToString(format));
            packet.Append("</Count>");
            packet.Append("</Range>");

            packet.Append(this.GetQueryPacketResultTypeElements());

            if (this.DisplayFields.Count > 0)
            {
                packet.Append("<Properties>");
                foreach (string field in this.DisplayFields)
                {
                    packet.Append("<Property name=\"" + field + "\"></Property>");
                }
                packet.Append("</Properties>");
            }

            if (this.SortFields.Count > 0)
            {
                packet.Append("<SortByProperties>");
                int count = 1;
                foreach (string field in this.SortFields)
                {
                    packet.Append("<SortByProperty name=\"" + field + "\" Direction=\"Ascending\" Order=\"" + count.ToString(format) + "\" />");
                    count = count + 1;
                }
                packet.Append("</SortByProperties>");
            }

            packet.Append("</Query>");
            packet.Append("</QueryPacket>");


            return packet.ToString();
        }

        /// <summary>
        /// Builds an Xml document to pass into a SharePoint Search
        /// service Query method for full text searches.
        /// </summary>
        /// <returns></returns>
        public string BuildFullTextQueryPacket()
        {
            StringBuilder packet = new StringBuilder();
            packet.Append("<?xml version=\"1.0\" encoding=\"utf-8\" ?>");
            packet.Append("<QueryPacket xmlns=\"urn:Microsoft.Search.Query\" Revision=\"1000\">");
            packet.Append("    <Query domain=\"QDomain\">");
            packet.Append("        <SupportedFormats>");
            packet.Append("            <Format>urn:Microsoft.Search.Response.Document.Document</Format>");
            packet.Append("        </SupportedFormats>");
            packet.Append("        <Context>");
            packet.Append("            <QueryText language=\"en-US\" type=\"MSSQLFT\">");
            packet.Append(this.SearchText);
            packet.Append("            </QueryText>");
            packet.Append("        </Context>");
            packet.Append("<Range>");
            packet.Append("<StartAt>");

            NumberFormatInfo format = CultureInfo.CurrentCulture.NumberFormat;

            packet.Append(this.StartAt.ToString(format));
            packet.Append("</StartAt>");
            packet.Append("<Count>");
            packet.Append(this.ResultCount.ToString(format));
            packet.Append("</Count>");
            packet.Append("</Range>");
            packet.Append(this.GetQueryPacketResultTypeElements());
            packet.Append("    </Query>");
            packet.Append("</QueryPacket> ");

            return packet.ToString();
        }

        string GetQueryPacketResultTypeElements()
        {
            if (this.SearchResultType == SearchResultType.Relevant)
            {
                return "<IncludeRelevantResults>true</IncludeRelevantResults>";
            }
            else
            {
                throw new ResultTypeNotSupportedException(
                    string.Format("The specified Result Type '{0}' is not supported with web service calls.",
                    this.SearchResultType.ToString()));
            }
        }


    }
}
